package com.magenta.maxoptra.geocoding.geoinformsputnik;

import com.magenta.maxoptra.gis.*;
import com.magenta.maxoptra.gis.commons.Pair;
import com.magenta.maxoptra.gis.commons.StringUtils;
import com.magenta.maxoptra.gis.utils.DoForwardGeocodingProfiler;
import com.magenta.mrs.basegeocoding.BaseGeocoding;
import com.magenta.mrs.util.GeoInformationClient;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.Credentials;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.params.ConnRoutePNames;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.*;
import java.io.*;
import java.net.Authenticator;
import java.net.InetSocketAddress;
import java.net.PasswordAuthentication;
import java.net.SocketAddress;
import java.util.*;
import java.util.zip.GZIPInputStream;

import static com.magenta.maxoptra.gis.GisParameter.*;

/**
 * Created with IntelliJ IDEA.
 * User: KuznetsovD
 * Date: 30.09.13
 * Time: 11:29
 * To change this template use File | Settings | File Templates.
 */
public class GeoInformSputnikProvider extends BaseGeocoding<String> implements IGeocoding {

    public static final String BR = "<br>";

    protected java.net.Proxy proxy;

    private static class GeoSamaraAddress {
        final String name;
        //        String nameWithCity;
        final String classifiedName;
        final double x;
        final double y;

        private GeoSamaraAddress(String name, String classifiedName, double x, double y) {
            this.name = name;
            this.classifiedName = classifiedName;
            this.x = x;
            this.y = y;
//            this.nameWithCity = getNameWithCity(name, classifiedName);
        }

        private String getNameWithCity() {
            return getNameWithCity(name);
        }

        private String getNameWithCity(String addressName) {
            if (GeoHelper.isTownAddress(addressName)) return addressName;
            final String town = GeoHelper.getTown(classifiedName);
            if (town != null) {
                return town + ", " + addressName;
            }
            return classifiedName;
        }
    }

    public String getProviderName() {
        return MapProviderType.geoinformsputnik.name();
    }

    @Override
    protected boolean accuracyIsAccurateEnough(String accuracyThreshold, String accuracy) {
        return false;
    }

    @Override
    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit, List<String> accuracies) throws GeoException {
        return findAddressByKeyword(settings, keyword, resultAddressLimit);
    }

    @Override
    public void setClient(GeoInformationClient client) {
        this.client = client;
        HttpHost httpProxy = (HttpHost) client.getParams().getParameter(ConnRoutePNames.DEFAULT_PROXY);
        if (httpProxy != null) {
            String proxyHost = httpProxy.getHostName();
            int proxyPort = httpProxy.getPort();
            final Credentials credentials = ((DefaultHttpClient) client).getCredentialsProvider().getCredentials(
                    new AuthScope(proxyHost, proxyPort));

            SocketAddress socketAddress = new InetSocketAddress(proxyHost, proxyPort);
            this.proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, socketAddress);

            Authenticator.setDefault(new Authenticator() {
                protected PasswordAuthentication getPasswordAuthentication() {
                    PasswordAuthentication p = new PasswordAuthentication(credentials.getUserPrincipal().getName(),
                            credentials.getPassword().toCharArray());
                    return p;
                }
            });
        }
    }

    protected List<GeoAddress> doFindAddressByKeyword(GeoProviderSettings settings, String region, String keyword, int resultAddressLimit, List<String> accuracies)
            throws GeoException {
        GeoAddress geoAddress = new GeoAddress();
        geoAddress.setState(region);
        geoAddress.setZip(keyword);
        return doForwardGeocoding(settings, geoAddress, resultAddressLimit);
    }

    public List<GeoAddress> findAddressByKeyword(GeoProviderSettings settings, String keyword, int resultAddressLimit) throws GeoException {
        DoForwardGeocodingProfiler profiler = new DoForwardGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), keyword);
        try {
            List<GeoAddress> addresses = updateCoordinates(keyword);
            profiler.logDuration(addresses.size());
            return addresses;
        } catch (Exception e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e);
        }
    }

    @Override
    public List<GeoAddress> doForwardGeocoding(GeoProviderSettings settings, GeoAddress address, int resultAddressLimit)
            throws GeoException {
        return findAddressByKeyword(settings, formatString(address), resultAddressLimit);
    }

    private String formatString(GeoAddress address) {
        String s = "";
        if (StringUtils.isNotEmpty(address.getCountry())) {
            s = s + ((StringUtils.isNotEmpty(address.getCountry()))? ", "+address.getCountry() : "");
            s = s + ((StringUtils.isNotEmpty(address.getState()))? ", "+address.getState() : "");
            s = s + ((StringUtils.isNotEmpty(address.getCity()))? ", "+address.getCity() : "");
            s = s + ((StringUtils.isNotEmpty(address.getStreet()))? ", "+address.getStreet() : "");
            s = s + ((StringUtils.isNotEmpty(address.getHouse()))? ", д."+address.getHouse() : "");
        }
        return s;
    }

    public GeoAddress findBestAddressByKeyword(GeoProviderSettings settings, String keyword) throws GeoException {
        DoForwardGeocodingProfiler profiler = new DoForwardGeocodingProfiler(getProviderName(), getAccountGlobalId(), isLoggingAllowed(), keyword);
        try {
            List<GeoAddress> addresses = updateCoordinates(keyword);
            GeoAddress geoAddress = addresses != null && addresses.size() > 0 ? addresses.get(0) : null;
            profiler.logDuration(geoAddress);
            return geoAddress;
        } catch (Exception e) {
            profiler.logError(e.getMessage());
            throw new GeoException(e);
        }
    }

    private List<GeoAddress> updateCoordinates(String address) {
        List<GeoAddress> result = new ArrayList<GeoAddress>();
        if ("".equals(address.trim())) {
            logger.info("Skip find address for empty address request");
            return result;
        }

        final String clearAddress;
        try {
            //String addr = GeoHelper.isTownAddress(address) ? address : (getGeocodingCity() + ", " + address);
            clearAddress = replaceKeyWords(address);

            String projection = getProjection();
            logger.debug("Address for request '" + clearAddress + "'");
            final List<GeoSamaraAddress> addresses = requestCoordinates(clearAddress);
            logger.debug("Founded " + addresses.size() + " addresses");

            final Map<GeoAddress, Integer> levenshteinDistances = new HashMap<GeoAddress, Integer>();
            final Set<String> addressNames = new HashSet<String>();
            for (GeoSamaraAddress geoSamaraAddress : addresses) {
                final String classifiedName = geoSamaraAddress.classifiedName;
                final String classifiedWOSpaces = classifiedName.replaceAll(" ", "");
                if (addressNames.contains(classifiedWOSpaces)) {
                    logger.debug("Address '" + classifiedName + "' already added in result list ");
                    continue;
                }
                addressNames.add(classifiedWOSpaces);
                GeoAddress shortAddress = new GeoAddress();
                if (!"EPSG::4326".equalsIgnoreCase(projection)) {
                    GeoPosition geoPosition = GeoSamaraCRSConverter.convert(geoSamaraAddress.x, geoSamaraAddress.y, projection);
                    shortAddress.setLatitude(geoPosition.getLatitude());
                    shortAddress.setLongitude(geoPosition.getLongitude());
                } else {
                    shortAddress.setLatitude(geoSamaraAddress.x);
                    shortAddress.setLongitude(geoSamaraAddress.y);
                }
                shortAddress.setInfo(geoSamaraAddress.classifiedName);
                final int distance = getLevenshteinDistance(clearAddress, geoSamaraAddress);
                levenshteinDistances.put(shortAddress, distance);
                logger.info("Founded address '%s' has precision: '%s'");
                result.add(shortAddress);
            }
            result = keepExactIfOne(result, levenshteinDistances);
        } catch (IOException e) {
            logger.error(e);
            e.printStackTrace();
        } catch (ParserConfigurationException e) {
            logger.error(e);
        } catch (SAXException e) {
            logger.error(e);
        } catch (XPathExpressionException e) {
            logger.error(e);
        }
        return result;
    }

    static List<GeoAddress> keepExactIfOne(List<GeoAddress> addresses, Map<GeoAddress, Integer> levenshteinDistances) {
        GeoAddress exactAddress = null;
        for (GeoAddress address : addresses) {
            final Integer distance = levenshteinDistances.get(address);
            if (distance != null && distance == 0) {
                if (exactAddress != null) {
                    // Если нашли несколько абсолютно точных резултатов, то возвращаем все как есть
                    return addresses;
                }
                exactAddress = address;
            }
        }
        if (exactAddress == null) {
            // not found absolutely exact addresses
            return addresses;
        }
        final List<GeoAddress> result = new ArrayList<GeoAddress>(1);
        result.add(exactAddress);
        return result;
    }


    private List<GeoSamaraAddress> requestCoordinates(String address) throws IOException, ParserConfigurationException, SAXException, XPathExpressionException {

        //address = URLEncoder.encode(address, "UTF-8");
        String url = getGeocodingURL();
        //String url1 = "http://map.samadm.ru/wfs1";
        String searchLayer = getSearchLayer();
        String projection = getProjection();
//        geosmr:EC_1_10
        String xml = "<wfs:GetFeature service=\"WFS\" version=\"2.0.0\" outputFormat=\"application/gml+xml; version=3.2\" xsi:schemaLocation=\"http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd\" xmlns:geosmr=\"http://www.geosamara.ru/wfs/geosmr/namespace\" xmlns:wfs=\"http://www.opengis.net/wfs/2.0\" xmlns:fes=\"http://www.opengis.net/fes/2.0\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n" +
                "  <wfs:Query typeNames=\"geosmr:" + searchLayer + "\" srsName=\""+projection+"\">\n" +
                "    <fes:Filter>\n" +
                "      <fes:PropertyIsLike wildCard=\"*\" singleChar=\"#\" escapeChar=\"!\">\n" +
                "        <fes:ValueReference>geosmr:Description</fes:ValueReference>\n" +
                "        <fes:Literal>" + address + "</fes:Literal>\n" +
                "      </fes:PropertyIsLike>\n" +
                "    </fes:Filter>\n" +
                "  </wfs:Query>\n" +
                "</wfs:GetFeature>";

        long start = System.currentTimeMillis();
        String resultStr = post(url, xml);

        logger.info("POST DURATION: %d msec. String length = %d", (System.currentTimeMillis() - start), resultStr.length());
        final DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();

        logger.info("Disable features and validating");
        docFactory.setNamespaceAware(false);
        docFactory.setValidating(false);
        docFactory.setFeature("http://xml.org/sax/features/namespaces", false);
        docFactory.setFeature("http://xml.org/sax/features/validation", false);
        docFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-dtd-grammar", false);
        docFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);

//        docFactory.setNamespaceAware(false);
        final DocumentBuilder builder = docFactory.newDocumentBuilder();

        byte[] bytes = resultStr.getBytes("UTF-8");

        ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);

        InputSource is = new InputSource(byteStream);

        long start1 = System.currentTimeMillis();
        final Document doc = builder.parse(is);
        logger.info("PARSE: %d msec", (System.currentTimeMillis() - start1));

        final XPathFactory factory = XPathFactory.newInstance();
        final XPath xpath = factory.newXPath();
        XPathExpression expr = xpath.compile("/FeatureCollection/member/" + searchLayer);
        NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
        logger.info("EVALUATE: %d msec", (System.currentTimeMillis() - start1));

        if (nodes.getLength() == 0) {
            logger.info(address + " not found");
            //return Collections.emptyList();
        }
        logger.info("For request '%s' Geosamara returned %d addresses", address, nodes.getLength());
        if (nodes.getLength() > 500) {
            logger.warn("For request '%s' Geosamara returned too many addresses. Skip this result", address);
            return new ArrayList<GeoSamaraAddress>();
        }

        XPathExpression textExpr = xpath.compile("description/text");
        XPathExpression addressNameExpr = xpath.compile("name");
        XPathExpression coordExpr = xpath.compile("geometry/Polygon/exterior/LinearRing/posList");

        List<GeoSamaraAddress> result = new ArrayList<GeoSamaraAddress>();
        for (int n = 0; n < nodes.getLength(); n++) {
            Node item = nodes.item(n);
            Node addressNode = (Node) textExpr.evaluate(item, XPathConstants.NODE);
            Node addressNameNode = (Node) addressNameExpr.evaluate(item, XPathConstants.NODE);
            Node coordNode = (Node) coordExpr.evaluate(item, XPathConstants.NODE);
            String addressNameContent = addressNameNode.getTextContent();
            String fullAddress = addressNode.getTextContent();
            fullAddress = fullAddress.replace("адрес:", "").replace("Адрес:", "").replace("АДРЕС:", "").trim();
            final int brIndex = fullAddress.indexOf(BR);
            if (brIndex >= 0) {
                fullAddress = fullAddress.substring(brIndex + BR.length());
            }

            double[] coordinates = parseCoordinates(coordNode.getTextContent());
            result.add(new GeoSamaraAddress(addressNameContent, fullAddress, coordinates[0], coordinates[1]));
        }

        final String a = address;
        Collections.sort(result, new Comparator<GeoSamaraAddress>() {
            @Override
            public int compare(GeoSamaraAddress o1, GeoSamaraAddress o2) {
                return Integer.valueOf(getLevenshteinDistance(a, o1))
                        .compareTo(getLevenshteinDistance(a, o2));
            }
        });
        return result;
    }

    private String post(String url, String message) throws IOException {
        HttpPost httpPost = new HttpPost(url);
        httpPost.addHeader("Accept-Encoding", "gzip,deflate,sdch");
        httpPost.setEntity(new StringEntity(message, "utf-8"));
        HttpEntity entity = client.execute(httpPost).getEntity();
        InputStream content = entity.getContent();
        if (entity.getContentEncoding() != null && "gzip".equals(entity.getContentEncoding().getValue())) {
            content = new GZIPInputStream(content);
        }
        Writer writer = new StringWriter();
        char[] buffer = new char[1024];
        try {
            Reader reader = new BufferedReader(new InputStreamReader(content, "UTF-8"));
            int n;
            while ((n = reader.read(buffer)) != -1) {
                writer.write(buffer, 0, n);
            }
        } finally {
            content.close();
        }
        String response = writer.toString();
        writer.close();
        //httpClient.getConnectionManager().shutdown();
        //System.out.println(response);
        return response;
    }


    private static int getLevenshteinDistance(String request, GeoSamaraAddress response) {
        final String[] addresses = response.name.split("/");
        int min = Integer.MAX_VALUE;
        for (String address : addresses) {
            min = Math.min(min, getLevenshteinDistance(request, response.getNameWithCity(address)));
        }
        return min;
    }

    private final static String[] removeStr = new String[]{"г.", "д.", "Самарская область", ",", ";", "ул.", "ул ", "--", "пр.", "проспект", "пр-кт", "бульвар", "бул.", "проезд",  " "};

    private static int getLevenshteinDistance(String request, String response) {
        request = request.toUpperCase();
        response = response.toUpperCase();
        for (String str : removeStr) {
            request = request.replace(str.toUpperCase(), "");
            response = response.replace(str.toUpperCase(), "");
        }

        request = request.trim();
        response = response.trim();

        return levenshteinDistance(request, response);
    }

    private static int levenshteinDistance(CharSequence str1,
                                           CharSequence str2) {
        // Алгоритм рассчета расстояния Левенштейна
        int[][] distance = new int[str1.length() + 1][str2.length() + 1];
        for (int i = 0; i <= str1.length(); i++) distance[i][0] = i;
        for (int j = 0; j <= str2.length(); j++) distance[0][j] = j;

        for (int i = 1; i <= str1.length(); i++)
            for (int j = 1; j <= str2.length(); j++)
                distance[i][j] = Math.min(Math.min(distance[i - 1][j] + 1, distance[i][j - 1] + 1),
                        distance[i - 1][j - 1] + ((str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1));
        return distance[str1.length()][str2.length()];
    }

    private static double[] parseCoordinates(String coordinates) {
        String[] coords = coordinates.split(" ");
        double minX = Double.valueOf(coords[0]);
        double minY = Double.valueOf(coords[1]);
        double maxX = minX;
        double maxY = minY;
        for (int c = 0; c < coords.length; c += 2) {
            Double x = Double.valueOf(coords[c]);
            Double y = Double.valueOf(coords[c + 1]);
            minX = Math.min(minX, x);
            minY = Math.min(minY, y);
            maxX = Math.min(maxX, x);
            maxY = Math.min(maxY, y);
        }
        return new double[]{(maxX + minX) / 2, (maxY + minY) / 2};
    }

    private final static List<Pair<String, String>> replaceWords = Arrays.asList(
            new Pair<String, String>("проспект", "пр."),
            new Pair<String, String>("Проспект", "пр."),
            new Pair<String, String>("бульвар", "бул."),
            new Pair<String, String>("Бульвар", "бул."),
            new Pair<String, String>("шоссе", "ш."),
            new Pair<String, String>("Шоссе", "ш.")
    );

    private final static String replaceKeyWords(String str) {
        for (Pair<String, String> replaceWord : replaceWords) {
            str = str.replace(replaceWord.getFirst(), replaceWord.getSecond());
        }
        return str;
    }


    private String getGeocodingURL() {
        return getGisStringProperty(geocoding_url);
    }

    private String getSearchLayer() {
        return getGisStringProperty(layers);
    }

    private String getProjection() {
        return getGisStringProperty(projection);
    }

}
